import 'dart:async';

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:html/parser.dart';
import 'package:riverpod_annotation/experimental/scope.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'common.dart';
import 'tag.dart';
import 'user.dart';

part 'question.g.dart';
part 'question.freezed.dart';

@freezed
sealed class QuestionsResponse with _$QuestionsResponse {
  factory QuestionsResponse({
    required List<Question> items,
    required int total,
  }) = _QuestionsResponse;

  factory QuestionsResponse.fromJson(Map<String, Object?> json) =>
      _$QuestionsResponseFromJson(json);
}

@freezed
sealed class Question with _$Question {
  @JsonSerializable(fieldRename: FieldRename.snake)
  factory Question({
    required List<String> tags,
    required int viewCount,
    required int score,
    int? bountyAmount,
    int? acceptedAnswerId,
    required User owner,
    required int answerCount,
    @TimestampParser() required DateTime creationDate,
    required int questionId,
    required String link,
    required String title,
    required String body,
  }) = _Question;

  factory Question.fromJson(Map<String, Object?> json) =>
      _$QuestionFromJson(json);
}

final paginatedQuestionsProvider = FutureProvider.autoDispose
    .family<QuestionsResponse, int>((ref, pageIndex) async {
      final cancelToken = CancelToken();
      ref.onDispose(cancelToken.cancel);

      final uri = Uri(
        scheme: 'https',
        host: 'api.stackexchange.com',
        path: '/2.2/questions',
        queryParameters: <String, Object>{
          'order': 'desc',
          'sort': 'creation',
          'site': 'stackoverflow',
          'filter': '!17vW1m9jnXcpKOO(p4a5Jj.QeqRQmvxcbquXIXJ1fJcKq4',
          'tagged': 'flutter',
          'pagesize': '50',
          'page': '${pageIndex + 1}',
        },
      );

      final response = await ref
          .watch(client)
          .getUri<Map<String, Object?>>(uri, cancelToken: cancelToken);

      final parsed = QuestionsResponse.fromJson(response.data!);
      final page = parsed.copyWith(
        items:
            parsed.items.map((e) {
              final document = parse(e.body);
              return e.copyWith(
                body: document.body!.text.replaceAll('\n', ' '),
              );
            }).toList(),
      );

      return page;
    });

/// A provider exposing the total count of questions
final questionsCountProvider = Provider.autoDispose((ref) {
  return ref
      .watch(paginatedQuestionsProvider(0))
      .whenData((page) => page.total);
});

@freezed
sealed class QuestionTheme with _$QuestionTheme {
  const factory QuestionTheme({
    required TextStyle titleStyle,
    required TextStyle descriptionStyle,
  }) = _QuestionTheme;
}

@riverpod
QuestionTheme questionTheme(Ref ref) {
  return const QuestionTheme(
    titleStyle: TextStyle(color: Color(0xFF3ca4ff), fontSize: 16),
    descriptionStyle: TextStyle(color: Color(0xFFe7e8eb), fontSize: 13),
  );
}

/// A scoped provider, exposing the current question used by [QuestionItem].
///
/// This is used as a performance optimization to pass a [Question] to
/// [QuestionItem], while still instantiating [QuestionItem] using the `const`
/// keyword.
///
/// This allows [QuestionItem] to rebuild less often.
/// By doing so, even when using [QuestionItem] in a [ListView], even if new
/// questions are obtained, previously rendered [QuestionItem]s won't rebuild.
///
/// This is an optional step. Since scoping is a fairly advanced mechanism,
/// it's entirely fine to simply pass the [Question] to [QuestionItem] directly.
@Riverpod(dependencies: [])
AsyncValue<Question> currentQuestion(Ref ref) {
  throw UnimplementedError();
}

/// A UI widget rendering a [Question].
///
/// That question will be obtained through [currentQuestion]. As such, it is
/// necessary to override that provider before using [QuestionItem].
@Dependencies([currentQuestion, tagTheme])
class QuestionItem extends HookConsumerWidget {
  const QuestionItem({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final question = ref.watch(currentQuestionProvider);
    final questionTheme = ref.watch(questionThemeProvider);

    return question.when(
      error: (error, stack) => const Center(child: Text('Error')),
      loading: () => const Center(child: Text('loading')),
      data: (question) {
        return ListTile(
          title: Text(question.title, style: questionTheme.titleStyle),
          subtitle: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const SizedBox(height: 5),
              Text(question.body, maxLines: 3, overflow: TextOverflow.ellipsis),
              const SizedBox(height: 12),
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Wrap(
                    spacing: 5,
                    runSpacing: 5,
                    children: [for (final tag in question.tags) Tag(tag: tag)],
                  ),
                  const SizedBox(height: 6),
                  Row(
                    children: [
                      Expanded(
                        child: PostInfo(
                          originalPoster: question.owner,
                          postCreationDate: question.creationDate,
                        ),
                      ),
                      UpvoteCount(question.score),
                      const SizedBox(width: 10),
                      AnswersCount(
                        question.answerCount,
                        accepted: question.acceptedAnswerId != null,
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
        );
      },
    );
  }
}

/// A "hook" (see flutter_hooks) for rendering how far a [DateTime] is from now
/// ("5 seconds ago", ...).
///
/// This hook also takes care of refreshing the UI when the label changes.
/// More particularly, it will check every minute if the label has changed.
String _useAskedHowLongAgo(DateTime creationDate) {
  final label = useState('');

  useEffect(() {
    void setLabel() {
      final now = DateTime.now();
      final diff = now.difference(creationDate);

      String value;
      if (diff.inDays > 1) {
        value = '${diff.inDays} days';
      } else if (diff.inHours > 0) {
        value = '${diff.inHours} hours';
      } else if (diff.inMinutes > 0) {
        value = '${diff.inMinutes} mins';
      } else {
        value = '${diff.inSeconds} seconds';
      }

      label.value = 'asked $value ago';
    }

    setLabel();
    final timer = Timer.periodic(const Duration(minutes: 1), (_) => setLabel());

    return timer.cancel;
  }, [creationDate]);

  return label.value;
}

class PostInfo extends HookConsumerWidget {
  const PostInfo({
    super.key,
    required this.originalPoster,
    required this.postCreationDate,
  });

  final User originalPoster;
  final DateTime postCreationDate;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final askedHowLongAgoLabel = _useAskedHowLongAgo(postCreationDate);

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          askedHowLongAgoLabel,
          style: const TextStyle(color: Color(0xFF9fa6ad), fontSize: 12),
        ),
        const SizedBox(height: 3),
        UserAvatar(owner: originalPoster),
      ],
    );
  }
}

/// A UI component for showing the answer count on a question
class AnswersCount extends StatelessWidget {
  const AnswersCount(this.answerCount, {super.key, required this.accepted});

  final int answerCount;
  final bool accepted;

  @override
  Widget build(BuildContext context) {
    final textStyle =
        accepted
            ? null
            : answerCount == 0
            ? const TextStyle(color: Color(0xffacb2b8))
            : const TextStyle(color: Color(0xff5a9e6f));
    return Container(
      decoration:
          answerCount > 0
              ? BoxDecoration(
                color: accepted ? const Color(0xff5a9e6f) : null,
                border: Border.all(color: const Color(0xff5a9e6f)),
                borderRadius: BorderRadius.circular(3),
              )
              : null,
      padding: const EdgeInsets.all(7),
      child: Column(
        children: [
          Text(answerCount.toString(), style: textStyle),
          Text('answers', style: textStyle),
        ],
      ),
    );
  }
}

/// A UI component for showing the upvotes count on a question
class UpvoteCount extends StatelessWidget {
  const UpvoteCount(this.upvoteCount, {super.key});

  final int upvoteCount;

  @override
  Widget build(BuildContext context) {
    const textStyle = TextStyle(color: Color(0xffacb2b8));

    return Padding(
      padding: const EdgeInsets.all(7),
      child: Column(
        children: [
          Text(upvoteCount.toString(), style: textStyle),
          const Text('votes', style: textStyle),
        ],
      ),
    );
  }
}
